//go:build ignore

// generate_bundle.go bundles a split OpenAPI specification into a single file.
//
// This tool performs two main operations:
// 1. Preprocesses path definitions by inlining external $ref files
// 2. Bundles the complete specification using libopenapi
//
// The preprocessing step is necessary because:
// - It resolves path-level $refs before the bundler processes them
// - It adjusts relative paths to maintain correct references after inlining
// - It ensures OpenAPI 3.0 compatibility (prevents 3.1 conversion)
//
// Usage:
//   go run generate_bundle.go -input openapi-split.yaml -output openapi-generated.yaml

package main

import (
	"flag"
	"fmt"
	"log"
	"os"
	"path/filepath"
	"strings"

	"github.com/pb33f/libopenapi"
	"github.com/pb33f/libopenapi/bundler"
	"github.com/pb33f/libopenapi/datamodel"
	"gopkg.in/yaml.v3"
)

var (
	input  = flag.String("input", "openapi-split.yaml", "Input OpenAPI file")
	output = flag.String("output", "openapi-generated.yaml", "Output bundled OpenAPI file")
)

func main() {
	flag.Parse()

	// Step 1: Preprocess paths to inline external references
	// This resolves path-level $refs and adjusts relative paths
	preprocessedSpec, err := preprocessPaths(*input)
	if err != nil {
		log.Fatalf("Failed to preprocess paths: %v", err)
	}

	// Step 2: Configure libopenapi for bundling
	config := datamodel.NewDocumentConfiguration()
	config.BasePath = "."
	config.ExtractRefsSequentially = true

	// Parse the preprocessed specification
	document, err := libopenapi.NewDocumentWithConfiguration(preprocessedSpec, config)
	if err != nil {
		log.Fatalf("Failed to parse OpenAPI document: %v", err)
	}

	// Build the OpenAPI v3 model
	v3Model, err := document.BuildV3Model()
	if err != nil {
		log.Fatalf("Failed to build v3 model: %v", err)
	}

	log.Printf("Model built successfully using version %s", v3Model.Model.Version)

	// Step 3: Bundle all references into a single document
	bundledBytes, err := bundler.BundleDocumentComposed(&v3Model.Model, nil)
	if err != nil {
		log.Fatalf("Failed to bundle document: %v", err)
	}

	// Add auto-generated comment header
	header := fmt.Sprintf(`# Code generated by generate_bundle.go; DO NOT EDIT.
# Source: %s

`, *input)

	// Prepend header to bundled content
	finalContent := append([]byte(header), bundledBytes...)

	err = os.WriteFile(*output, finalContent, 0644)
	if err != nil {
		log.Fatalf("Failed to write output file %s: %v", *output, err)
	}

	fmt.Printf("Successfully bundled OpenAPI spec to %s\n", *output)
}

// preprocessPaths reads the OpenAPI spec and resolves path $refs inline
func preprocessPaths(inputFile string) ([]byte, error) {
	// Read the original file
	data, err := os.ReadFile(inputFile)
	if err != nil {
		return nil, fmt.Errorf("failed to read input file: %w", err)
	}

	// Parse the YAML
	var spec map[string]interface{}
	if err := yaml.Unmarshal(data, &spec); err != nil {
		return nil, fmt.Errorf("failed to parse yaml: %w", err)
	}

	// Process paths to inline $refs
	if paths, ok := spec["paths"].(map[string]interface{}); ok {
		newPaths := make(map[string]interface{})

		for pathKey, pathValue := range paths {
			if pathMap, ok := pathValue.(map[string]interface{}); ok {
				if ref, ok := pathMap["$ref"].(string); ok {
					// Read the referenced file
					refData, err := os.ReadFile(ref)
					if err != nil {
						return nil, fmt.Errorf("failed to read path ref %s: %w", ref, err)
					}

					var pathContent interface{}
					if err := yaml.Unmarshal(refData, &pathContent); err != nil {
						return nil, fmt.Errorf("failed to parse path ref %s: %w", ref, err)
					}

					// Fix relative refs in the inlined content
					refDir := filepath.Dir(ref)
					pathContent = fixRelativeRefs(pathContent, refDir)

					// Replace the $ref with the actual content
					newPaths[pathKey] = pathContent
				} else {
					// Keep as-is if no $ref
					newPaths[pathKey] = pathValue
				}
			} else {
				// Keep as-is if not a map
				newPaths[pathKey] = pathValue
			}
		}

		// Replace the entire paths object
		spec["paths"] = newPaths
	}

	// Marshal back to YAML
	preprocessedYAML, err := yaml.Marshal(spec)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal preprocessed spec: %w", err)
	}

	return preprocessedYAML, nil
}

// fixRelativeRefs recursively fixes relative $refs in the data structure
func fixRelativeRefs(data interface{}, baseDir string) interface{} {
	switch v := data.(type) {
	case map[string]interface{}:
		result := make(map[string]interface{})
		for key, value := range v {
			if key != "$ref" {
				result[key] = fixRelativeRefs(value, baseDir)
				continue
			}

			refStr, ok := value.(string)

			if !ok {
				result[key] = value
				continue
			}

			// Only fix relative file refs, not internal refs (#/) or URLs
			if !strings.HasPrefix(refStr, "#/") && !strings.HasPrefix(refStr, "http") && strings.HasSuffix(refStr, ".yaml") {
				// Convert relative path to be relative from the root
				newRef := filepath.Join(baseDir, refStr)
				newRef = filepath.Clean(newRef)
				// Make sure it starts with ./ for consistency
				if !strings.HasPrefix(newRef, "./") && !strings.HasPrefix(newRef, "../") {
					newRef = "./" + newRef
				}

				result[key] = newRef
				continue
			}

			result[key] = value
		}

		return result
	case []interface{}:
		result := make([]interface{}, len(v))
		for i, item := range v {
			result[i] = fixRelativeRefs(item, baseDir)
		}
		return result
	default:
		return v
	}
}
